﻿
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using TestGame.GameLib;

namespace TestGame {
    /// <summary>
    /// The PeerMultiplayerScreen is used to connect two Players together 
    /// using a simple peer-to-peer connection scheme. If either the players
    /// lose connection, the game is ended on both sides.
    /// </summary>
    class PeerMultiplayerScreen : SinglePlayerScreen {
        #region Fields

        /// <summary>
        /// The secondaryPlayer, also known as the remote player.
        /// </summary>
        PlayingArea secondaryPlayer;
        
        /// <summary>
        /// The IP information of the connected player.
        /// </summary>
        IPEndPoint ipInfo;
        
        /// <summary>
        /// A simple boolean used to keep track if the user is hosting the 
        /// game session or not.
        /// </summary>
        bool isHost;

        /// <summary>
        /// The UDP connection used to send and receive game updates from the
        /// remote player.
        /// </summary>
        UdpClient connection;

        #endregion

        #region Constructor

        /// <summary>
        /// Constructors a PeerMultiplayerScreen for two Players.
        /// </summary>
        public PeerMultiplayerScreen(IPEndPoint ipInfo, bool isHost) : base() {
            connection = null;
            pauseOnLostFocus = false;
            secondaryPlayer = new PlayingArea();

            this.ipInfo = ipInfo;
            this.isHost = isHost;

            SetupTCPConnection();
        }

        #endregion

        #region TCP Connection

        /// <summary>
        /// Creates a TCP connection based off if the current Player is the 
        /// host or client. 
        /// </summary>
        void SetupTCPConnection() {
            TcpListener host = null;
            TcpClient client = null;
            AsyncCallback callback;

            try {
                if (isHost) {
                    host = new TcpListener(IPAddress.Parse("127.0.0.1"), ipInfo.Port);
                    callback = new AsyncCallback(AcceptCallback);

                    host.Start();
                    host.BeginAcceptSocket(AcceptCallback, host);
                }
                else {
                    client = new TcpClient();
                    callback = new AsyncCallback(ConnectCallback);

                    client.BeginConnect("127.0.0.1", ipInfo.Port, callback, client);
                }
            }
            catch (Exception e) {
                ErrorHelper.HandleError(e, ErrorHandler);

                if (host != null) {
                    host.Stop();
                }
                else if (client != null) {
                    client.Close();
                }
            }
        }

        /// <summary>
        /// The Callback to be called once a Host finally has a connection from
        /// a peer. It will finish the Accept call and then exchange 
        /// information pertaining to GameRules. Once all the TCP information
        /// has been exchanged, the TCP connection is closed, and a new UDP 
        /// connection is opened.
        /// </summary>
        /// <param name="ar">Passes a TcpListener in the AsyncState</param>
        void AcceptCallback(IAsyncResult ar) {
            Random rng = new Random();
            int seed = rng.Next(int.MaxValue);
            TcpListener host = null;
            Socket client = null;

            try {
                host = (TcpListener)ar.AsyncState;
                client = host.EndAcceptSocket(ar);
                
                client.Send(NetworkHelper.PackHandshakeHeader(seed));

                byte[] buffer = new byte[100];
                int ackSize = client.Receive(buffer);

                OpenUDPConnection();
            }
            catch (Exception e) {
                ErrorHelper.HandleError(e, ErrorHandler);
            }
            finally {
                if (client != null) {
                    client.Close();
                }

                if (host != null) {
                    host.Stop();
                }
            }
        }

        /// <summary>
        /// This Callback to be called once a Client finally connects to a host
        /// peer. It will finish the Connect call and then exchange information
        /// pertaining to GameRules. Once all the TCP information has been 
        /// exchanged, the TCP connection is closed and a new UDP connection
        /// is opened.
        /// </summary>
        /// <param name="ar">Passes a TCP client in the AsyncState</param>
        void ConnectCallback(IAsyncResult ar) {
            TcpClient client = (TcpClient)ar.AsyncState;
            Stream server = null;
            byte[] buffer = new byte[100];
            int seed = 0;
            int size = 0;

            try {
                client = (TcpClient)ar.AsyncState;
                client.EndConnect(ar);

                server = client.GetStream();
                size = server.Read(buffer, 0, 100);

                NetworkHelper.UnpackHandshakeHeader(buffer, out seed);
                Console.WriteLine("Received Seed: " + seed);

                ASCIIEncoding encoding = new ASCIIEncoding();
                buffer = encoding.GetBytes("OK");

                server.Write(buffer, 0, buffer.Length);
                server.Flush();

                OpenUDPConnection();
            }
            catch (SocketException e) {
                if (e.SocketErrorCode == SocketError.ConnectionRefused) {
                    SetupTCPConnection();
                }
                else {
                    ErrorHelper.HandleError(e, ErrorHandler);
                }
            }
            catch (Exception e) {
                ErrorHelper.HandleError(e, ErrorHandler);
            }
            finally {
                if (client != null) {
                    client.Close();
                }

                if (server != null) {
                    server.Close();
                }
            }
        }

        #endregion

        #region UDP Connection

        /// <summary>
        /// Opens a UDP connection between the two peers. The only difference 
        /// between the host and the client, is which ports they use. The host
        /// sends on StandardPort and receives on StandardPort+1. The client 
        /// sends on StandardPort+1 and receives on StandardPort.
        /// </summary>
        void OpenUDPConnection() {
            try {
                if (isHost) {
                    connection = new UdpClient(ipInfo.Port);
                    connection.Connect("127.0.0.1", ipInfo.Port + 1);
                }
                else {
                    connection = new UdpClient(ipInfo.Port + 1);
                    connection.Connect("127.0.0.1", ipInfo.Port);
                }

                connection.BeginReceive(new AsyncCallback(ReceiveCallback), connection);
            }
            catch (SocketException socketError) {
                ErrorHelper.HandleError(socketError, ErrorHandler);
            }
        }

        /// <summary>
        /// The Receive callback that is invoked whenever the user receives a 
        /// UDP datagram on a specific port. Upon receiving the datagram, the 
        /// Method cuts it up and updates the appropriate data for the local
        /// player's representation of the remote player.
        /// </summary>
        /// <param name="ar"></param>
        void ReceiveCallback(IAsyncResult ar) {
            try {
                IPEndPoint clientIP = new IPEndPoint(IPAddress.Any, ipInfo.Port);
                byte[] buffer = connection.EndReceive(ar, ref clientIP);
                Console.WriteLine("Received {0} bytes from player.", buffer.Length);

                secondaryPlayer.ImportData(buffer);
                connection.BeginReceive(new AsyncCallback(ReceiveCallback), connection);
            }
            catch (SocketException socketError) {
                ErrorHelper.HandleError(socketError, ErrorHandler);
            }
        }

        #endregion

        #region Error Handling

        /// <summary>
        /// A simple handler that will exit the screen when an error occurs.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ErrorHandler(object sender, PlayerIndexEventArgs e) {
            ExitScreen();
        }

        #endregion

        #region Game Methods

        /// <summary>
        /// The overridden Method that is used in Multiplayer. There is no
        /// additional Update() calls, but the Method sends data over a UDP
        /// connection for the primary player (local player)'s blobs.
        /// </summary>
        /// <param name="time"></param>
        protected override void ComponentUpdate(GameTime time) {
            base.ComponentUpdate(time);

            // If there is a connection, then we send the UDP data
            if (connection != null) {
                byte[] data = primaryPlayer.ExportData();
                connection.Send(data, data.Length);
            }
        }
        
        /// <summary>
        /// The overridden Method that is ued in Multiplayer. This Method 
        /// makes no base() calls for the original, and nicely draws both 
        /// Players in seperated areas.
        /// </summary>
        /// <param name="time"></param>
        public override void ComponentDraw(GameTime time) {
            SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
            ParticleSystem.Draw(spriteBatch, content);

            // Draw the local Player
            PlayingArea.GraphicOffset = new Vector2(15, 15);
            primaryPlayer.Draw(spriteBatch, content);

            // Draw the remote Player
            PlayingArea.GraphicOffset = new Vector2(450, 15);
            secondaryPlayer.Draw(spriteBatch, content);
        }

        #endregion
    }
}
